/*
    SPDX-FileCopyrightText: 2017 Klarälvdalens Datakonsult AB a KDAB Group company <info@kdab.com>

    Work sponsored by the LiMux project of the city of Munich.
    SPDX-FileContributor: Andras Mantia <andras.mantia@kdab.com>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <array>

#include "screenmappertest.h"
#include "screenmapper.h"

#include <QSignalSpy>
#include <QTest>

#include <KActivities/Consumer>

#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
constexpr inline QLatin1String operator"" _L1(const char *str, size_t size) noexcept
{
    return QLatin1String(str, size);
}
#else
using namespace Qt::StringLiterals;
#endif

QTEST_MAIN(ScreenMapperTest)

void ScreenMapperTest::initTestCase()
{
    m_screenMapper = ScreenMapper::instance();
    m_currentActivity = KActivities::Consumer().currentActivity();
    m_alternativeActivity = QStringLiteral("a779f58a-b5e3-426d-9994-a8c54d26d528"); // Generated by uuidgen
    Q_ASSERT(m_currentActivity != m_alternativeActivity);
}

void ScreenMapperTest::init()
{
    m_screenMapper->cleanup();
}

void ScreenMapperTest::tst_addScreens()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    QSignalSpy s(m_screenMapper, &ScreenMapper::screensChanged);
    m_screenMapper->addScreen(-1, m_currentActivity, path);
    QCOMPARE(s.count(), 0);
    m_screenMapper->addScreen(1, m_currentActivity, path);
    QCOMPARE(s.count(), 1);
    m_screenMapper->addScreen(0, m_currentActivity, path);
    QCOMPARE(s.count(), 2);
    m_screenMapper->addScreen(1, m_currentActivity, path);
    QCOMPARE(s.count(), 2);

    // Test if activity ID works
    m_screenMapper->addScreen(1, m_alternativeActivity, path);
    QCOMPARE(s.count(), 3);
    m_screenMapper->addScreen(3, m_alternativeActivity, path);
    QCOMPARE(s.count(), 4);

    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 0);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_alternativeActivity), 1);
}

void ScreenMapperTest::tst_removeScreens()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    addScreens(path, m_currentActivity);
    addScreens(path, m_alternativeActivity);
    QSignalSpy s(m_screenMapper, &ScreenMapper::screensChanged);
    m_screenMapper->removeScreen(-1, m_currentActivity, path);
    QCOMPARE(s.count(), 0);
    m_screenMapper->removeScreen(1, m_currentActivity, path);
    QCOMPARE(s.count(), 1);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 0);
    m_screenMapper->removeScreen(1, m_currentActivity, path);
    QCOMPARE(s.count(), 1);
    m_screenMapper->addScreen(3, m_currentActivity, path);
    QCOMPARE(s.count(), 2);
    m_screenMapper->removeScreen(0, m_currentActivity, path);
    QCOMPARE(s.count(), 3);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 2);

    // Remove screens on m_currentActivity should not affect screens on m_alternativeActivity
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_alternativeActivity), 0);
    m_screenMapper->removeScreen(2, m_alternativeActivity, path);
    QCOMPARE(s.count(), 4);

    // Remove screens on m_alternativeActivity should not affect screens on m_currentActivity
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 2);
}

void ScreenMapperTest::tst_addMapping()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    addScreens(path, m_currentActivity);
    addScreens(path, m_alternativeActivity);
    QSignalSpy s(m_screenMapper, &ScreenMapper::screenMappingChanged);
    QString file(QStringLiteral("desktop:/foo%1.txt"));

    for (int i = 0; i < 3; i++) {
        const QUrl url = ScreenMapper::stringToUrl(file.arg(i));
        m_screenMapper->addMapping(url, i, m_currentActivity);
        QCOMPARE(s.count(), i + 1);
        QCOMPARE(m_screenMapper->screenForItem(url, m_currentActivity), i);

        // Mappings added to m_currentActivity should not affect mappings on m_alternativeActivity
        QCOMPARE(m_screenMapper->screenForItem(url, m_alternativeActivity), -1);
    }
}

void ScreenMapperTest::tst_addRemoveScreenWithItems()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    addScreens(path, m_currentActivity);
    addScreens(path, m_alternativeActivity);
    QString file(QStringLiteral("desktop:/foo%1.txt"));

    for (int i = 0; i < 3; i++) {
        const QUrl url = ScreenMapper::stringToUrl(file.arg(i));
        for (const QString &activity : {m_currentActivity, m_alternativeActivity}) {
            m_screenMapper->addMapping(url, i, activity);
        }
    }

    // remove one screen
    m_screenMapper->removeScreen(1, m_currentActivity, path);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(0)), m_currentActivity), 0);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(1)), m_currentActivity), -1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(2)), m_currentActivity), 2);
    // Remove a screen of m_currentActivity should not affect screens of m_alternativeActivity
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(1)), m_alternativeActivity), 1);

    // add removed screen back, items screen is restored
    m_screenMapper->addScreen(1, m_currentActivity, path);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(0)), m_currentActivity), 0);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(1)), m_currentActivity), 1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(2)), m_currentActivity), 2);
    // Restore a screen of m_currentActivity should not affect screens of m_alternativeActivity
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(1)), m_alternativeActivity), 1);

    // remove all screens, firstAvailableScreen changes
    m_screenMapper->removeScreen(0, m_currentActivity, path);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 1);
    m_screenMapper->removeScreen(1, m_currentActivity, path);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 2);
    m_screenMapper->removeScreen(2, m_currentActivity, path);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), -1);
    // Remove screens of m_currentActivity should not affect screens of m_alternativeActivity
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_alternativeActivity), 0);

    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(0)), m_currentActivity), -1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(1)), m_currentActivity), -1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(2)), m_currentActivity), -1);

    // add all screens back, all item's screen is restored
    addScreens(path, m_currentActivity);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(0)), m_currentActivity), 0);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(1)), m_currentActivity), 1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(file.arg(2)), m_currentActivity), 2);

    // remove one screen and move its item
    const QUrl movedItem = ScreenMapper::stringToUrl(file.arg(1));
    m_screenMapper->removeScreen(1, m_currentActivity, path);
    QCOMPARE(m_screenMapper->screenForItem(movedItem, m_currentActivity), -1);
    m_screenMapper->addMapping(movedItem, 0, m_currentActivity);
    QCOMPARE(m_screenMapper->screenForItem(movedItem, m_currentActivity), 0);
    QCOMPARE(m_screenMapper->screenForItem(movedItem, m_alternativeActivity), 1);

    // add back the screen, item goes back to the original place
    m_screenMapper->addScreen(1, m_currentActivity, path);
    QCOMPARE(m_screenMapper->screenForItem(movedItem, m_currentActivity), 1);
    QCOMPARE(m_screenMapper->screenForItem(movedItem, m_alternativeActivity), 1);
}

void ScreenMapperTest::tst_addRemoveScreenDifferentPaths()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/Foo"));
    const auto path2 = ScreenMapper::stringToUrl(QStringLiteral("desktop:/Foo2"));
    m_screenMapper->addScreen(0, m_currentActivity, path);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path, m_currentActivity), 0);
    QCOMPARE(m_screenMapper->firstAvailableScreen(path2, m_currentActivity), -1);
}

void ScreenMapperTest::tst_readScreenActivityMapping()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    addScreens(path, m_currentActivity);
    addScreens(path, m_alternativeActivity);

    QString file(QStringLiteral("desktop:/foo%1.txt"));
    const QStringList paths{
        file.arg(0),
        file.arg(1),
    };
    // clang-format off
    const QStringList mapping{
        paths[0], "0"_L1, m_currentActivity,
        paths[1], "1"_L1, m_currentActivity,
        paths[0], "1"_L1, m_alternativeActivity,
    };
    // clang-format on
    QSignalSpy s(m_screenMapper, &ScreenMapper::screenMappingChanged);

    m_screenMapper->setScreenMapping(mapping);
    QCOMPARE(s.count(), 1);

    // Check if the config is loaded correctly
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(paths[0]), m_currentActivity), 0);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(paths[1]), m_currentActivity), 1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(paths[0]), m_alternativeActivity), 1);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(paths[1]), m_alternativeActivity), -1);
}

void ScreenMapperTest::tst_readScreenActivityMappingFromOldConfig()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    addScreens(path, m_currentActivity);

    QString file(QStringLiteral("desktop:/foo%1.txt"));
    const QStringList paths{
        file.arg(0),
        file.arg(1),
    };
    // clang-format off
    const QStringList mapping{
        paths[0], "0"_L1,
        paths[1], "1"_L1,
    };
    // clang-format on
    QSignalSpy s(m_screenMapper, &ScreenMapper::screenMappingChanged);

    m_screenMapper->setScreenMapping(mapping);
    QCOMPARE(s.count(), 1);

    // Check if the config is loaded correctly
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(paths[0]), m_currentActivity), 0);
    QCOMPARE(m_screenMapper->screenForItem(ScreenMapper::stringToUrl(paths[1]), m_currentActivity), 1);
}

void ScreenMapperTest::tst_saveScreenActivityMapping()
{
    const auto path = ScreenMapper::stringToUrl(QStringLiteral("desktop:/"));
    addScreens(path, m_currentActivity);
    addScreens(path, m_alternativeActivity);

    QString file(QStringLiteral("desktop:/foo%1.txt"));
    const std::array<QStringList, 3> expectedMapping{
        QStringList{file.arg(0), "0"_L1, m_currentActivity},
        QStringList{file.arg(1), "1"_L1, m_currentActivity},
        QStringList{file.arg(0), "1"_L1, m_alternativeActivity},
    };

    for (const auto &l : expectedMapping) {
        m_screenMapper->addMapping(ScreenMapper::stringToUrl(l[0]), l[1].toInt(), l[2]);
    }

    const QStringList mapping = m_screenMapper->screenMapping();

    QCOMPARE(mapping.count(), expectedMapping.size() * expectedMapping[0].size());

    for (int i = 0; i < mapping.count() - (expectedMapping[0].size() - 1); i += expectedMapping[0].size()) {
        const QStringList configGroup{mapping[i], mapping[i + 1], mapping[i + 2]};
        QVERIFY(std::find(std::cbegin(expectedMapping), std::cend(expectedMapping), configGroup) != std::cend(expectedMapping));
    }
}

void ScreenMapperTest::tst_readAndSaveItemsOnActivitiesOnDisabledScreens()
{
    QString file(QStringLiteral("desktop:/foo%1.txt"));
    const QStringList paths{
        ScreenMapper::stringToUrl(file.arg(0)).toString(),
        ScreenMapper::stringToUrl(file.arg(1)).toString(),
        ScreenMapper::stringToUrl(file.arg(2)).toString(),
        ScreenMapper::stringToUrl(file.arg(3)).toString(),
    };
    // clang-format off
    const std::array<QStringList, 3> expectedMapping{
        QStringList{"0"_L1, m_currentActivity, "2"_L1, paths[0], paths[1]},
        QStringList{"1"_L1, m_currentActivity, "2"_L1, paths[2], paths[3]},
        QStringList{"0"_L1, m_alternativeActivity, "2"_L1, paths[0], paths[1]},
    };
    // clang-format on
    QStringList seralizedMap;

    // Create a seralized QStringList from expectedMapping
    for (const auto &l : expectedMapping) {
        for (const auto &s : l) {
            seralizedMap.append(s);
        }
    }

    m_screenMapper->readDisabledScreensMap(seralizedMap);

    // Check the actual mapping result
    const QStringList mapping = m_screenMapper->disabledScreensMap();

    QCOMPARE(mapping.count(), expectedMapping.size() * expectedMapping[0].size());

    for (int i = 0; i < mapping.count() - (expectedMapping[0].size() - 1); i += expectedMapping[0].size()) {
        const QStringList configGroup{mapping[i], mapping[i + 1], mapping[i + 2], mapping[i + 3], mapping[i + 4]};
        QVERIFY(std::find(std::cbegin(expectedMapping), std::cend(expectedMapping), configGroup) != std::cend(expectedMapping));
    }
}

void ScreenMapperTest::tst_readAndSaveItemsOnActivitiesOnDisabledScreensFromOldConfig()
{
    QString file(QStringLiteral("desktop:/foo%1.txt"));
    const QStringList paths{
        ScreenMapper::stringToUrl(file.arg(0)).toString(),
        ScreenMapper::stringToUrl(file.arg(1)).toString(),
        ScreenMapper::stringToUrl(file.arg(2)).toString(),
        ScreenMapper::stringToUrl(file.arg(3)).toString(),
    };
    // clang-format off
    const std::array<QStringList, 2> expectedMapping{
        QStringList{"0"_L1, m_currentActivity, "2"_L1, paths[0], paths[1]},
        QStringList{"1"_L1, m_currentActivity, "2"_L1, paths[2], paths[3]},
    };
    // clang-format on
    QStringList seralizedMap;

    // Create a seralized QStringList from expectedMapping
    for (const auto &l : expectedMapping) {
        for (const auto &s : l) {
            if (s == m_currentActivity) {
                continue; // Missing activity ID from the old config before 5.25
            }

            seralizedMap.append(s);
        }
    }

    m_screenMapper->readDisabledScreensMap(seralizedMap);

    // Check the actual mapping result
    const QStringList mapping = m_screenMapper->disabledScreensMap();

    QCOMPARE(mapping.count(), expectedMapping.size() * expectedMapping[0].size());

    for (int i = 0; i < mapping.count() - (expectedMapping[0].size() - 1); i += expectedMapping[0].size()) {
        const QStringList configGroup{mapping[i], mapping[i + 1], mapping[i + 2], mapping[i + 3], mapping[i + 4]};
        QVERIFY(std::find(std::cbegin(expectedMapping), std::cend(expectedMapping), configGroup) != std::cend(expectedMapping));
    }
}

void ScreenMapperTest::addScreens(const QUrl &path, const QString &activity)
{
    for (int screen = 0; screen < 3; screen++) {
        m_screenMapper->addScreen(screen, activity, path);
    }
}
